import asyncio
import logging
import socket
import threading
import uuid
import time
import traceback
import struct
from typing import Dict
from ssdp.aio import SSDP
from ssdp.messages import SSDPRequest, SSDPResponse

import expiringSet
import util
import random

# ip_v4 组播地址
SSDP_GROUP_IP4 = "239.255.255.250"
SSDP_GROUP_IP4 = "239.0.0.123"
SSDP_PORT = 21900
SSDP_END_POINT = (SSDP_GROUP_IP4, SSDP_PORT)

ANY_IP = "0.0.0.0" if util.is_linux() else util.get_local_ip()


# logging.basicConfig(
#     format="%(asctime)s,%(msecs)03d [%(levelname)s] [%(threadName)s] [%(name)s:%(lineno)s] : %(message)s",
#     datefmt="%Y-%m-%d %H:%M:%S",
#     level=logging.INFO,
# )

logger = logging.getLogger(__name__)

logger.info("******************************************************")
logger.info(f"any_ip: {ANY_IP}")
logger.info("******************************************************")


class SsdpDevice(SSDP):
    """
    即是控制点(Client), 又是设备(Server)
    """

    SSDP_EXPIRE = 3

    # Unique Service Name
    SEARCH_TARGET = "urn:menusifu:service:posmeshAppClient:1"  # ST

    def __init__(self):
        self.device_id = uuid.uuid4()  # 设备id
        self.peers = expiringSet.ExpiringSet(
            ttl=self.SSDP_EXPIRE,
            added_callbacks=[self.__on_peers_added],
            expired_callbacks=[self.__on_peers_removed],
        )

        self._loop = asyncio.get_event_loop()
        self._should_stop = threading.Event()
        self.started = False

        self.ip = util.get_local_ip()

    # -------------------------------------------------------------------------------------

    def connection_made(self, transport: asyncio.transports.DatagramTransport) -> None:
        self.transport = transport
        super().connection_made(transport)

    def response_received(self, response, addr: tuple) -> None:
        """
        收到ssdp:discover的响应: 每个响应对应一个设备发现

        新/旧设备都有可能
        """

        logger.debug(f"{'>'*5} response from {addr}")
        self.peers.add(addr[0])

    def request_received(self, request, addr: tuple) -> None:
        """
        收到请求(只处理ssdp:discover)，若本设备是查询目标，则作出响应，否则忽略；
        """

        print(f"--------------------------------------------------------- {addr}")

        headers = dict(request.headers)
        if headers.get("MAN", "").strip('"') == "ssdp:discover":  # 隐式地作为一个设备
            return self.__on_discover_request(headers, addr)

    def __on_discover_request(self, headers: Dict[str, any], addr: tuple) -> None:
        if headers.get("ST", "").strip('"') not in (
            "ssdp:all",
            self.__class__.SEARCH_TARGET,
        ):
            return

        # 返回自身设备信息
        resp = SSDPResponse(
            200,
            "OK",
            headers={
                "Cache-Control": "max-age=30",
                "Location": self.ip,
                "Server": f"Python UPnP/1.0 {self.__class__.__name__}",
                "ST": self.__class__.SEARCH_TARGET,
                "USN": f"uuid:{self.device_id}::{self.__class__.SEARCH_TARGET}",
                "EXT": "",
            },
        )

        try:
            resp.sendto(self.transport, addr)
            logger.debug(f"{'<'*5} response to {addr}")
        except Exception:
            data = bytes(resp)
            logger.error(
                f"------------------- ssdp response size: {len(data)}, address:{addr}, data={data}"
            )
            logger.error(traceback.format_exc())

    # -------------------------------------------------------------------------------------

    def __on_peers_added(self, new_peers):
        logger.info(f"new peers added: {new_peers}")
        if hasattr(self, "member_changed") and (new_peers):
            self.member_changed(new_peers, set())

    def __on_peers_removed(self, removed_peers):
        logger.info(f"peers removed: {removed_peers}")
        if hasattr(self, "member_changed") and (removed_peers):
            self.member_changed(set(), removed_peers)

    # -------------------------------------------------------------------------------------

    def __build_ssdp_socket(self):
        """
        构建udp socket, 加入到指定多播组
        """
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 一个_search请求大概150个字节
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024 * 512)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 1024)

        ttl = struct.pack('b', 5)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 0)
        
        mreq = struct.pack(
            "4s4s", socket.inet_aton(SSDP_GROUP_IP4), socket.inet_aton(ANY_IP)
        )
        sock.setsockopt(
            socket.IPPROTO_IP,
            socket.IP_ADD_MEMBERSHIP,
            mreq,
        )
        sock.settimeout(30)
        sock.bind((ANY_IP, SSDP_PORT))

        return sock

    def stop(self):
        self._should_stop.set()
        logger.debug("stop called")

    def start(self):
        """
        启动设备,定时向多播组内发送ssdp:discover请求
        """
        if self.started:
            logger.debug("device already start")
            return

        self.started = True
        self._should_stop.clear()

        threading.Thread(
            name="thread-ssdp_discover",
            target=lambda: self._loop.run_until_complete(self.__search()),
        ).start()

        logger.debug("device started")

    async def __search(self):
        transport = (
            await self._loop.create_datagram_endpoint(
                lambda: self,  # protocol_factory
                sock=self.__build_ssdp_socket(),
            )
        )[0]

        # 定时向指定的 ip:port 发送search请求
        sleep_time = 1 + random.random()
        while not self._should_stop.is_set():
            try:
                search_request = SSDPRequest(
                    "M-SEARCH",  # https://datatracker.ietf.org/doc/html/rfc2774#section-5
                    headers={
                        "HOST": f"{SSDP_GROUP_IP4}:{SSDP_PORT}",
                        "MAN": '"ssdp:discover"',
                        "MX": 5,
                        "ST": self.SEARCH_TARGET,
                        "SERVER": socket.gethostname(),
                    },
                )
                search_request.sendto(transport, SSDP_END_POINT)
                logger.debug(
                    f"search req sent, {self.ip}, size:{len(bytes(search_request))}"
                )

                await asyncio.sleep(sleep_time)
            except Exception:
                pass

        transport.close()
        logger.debug("device stopped")
        self.started = False
